package org.bagou.proj.plugin

import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import javassist.*
import javassist.bytecode.ClassFile
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.gradle.api.Project

import java.lang.reflect.Modifier
import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream

class TinyPngPluginTransform extends Transform {

    private ClassPool classPool = ClassPool.getDefault()
    TinyPngPluginTransform(Project project){
        // 为了能够直接找到安卓相关的类，需要把android.jar包路径添加到 classPool 类搜索路径中去。
        classPool.appendClassPath(project.android.bootClasspath[0].toString())

        classPool.importPackage("android.os.Bundle")
        classPool.importPackage("android.widget.Toast")
        classPool.importPackage("android.app.Activity")

        classPool.importPackage("android.view.View")

        // 新增ImageView大图检测的字节码插装，需要导包
        // 这里做导包的目的是，下面通过 classPool.get("包名") 能获得对应的CtClass
        classPool.importPackage("android.graphics.drawable.Drawable")
        classPool.importPackage("androidx.appcompat.widget.AppCompatImageView")
        classPool.importPackage("android.widget.ImageView")
        classPool.importPackage("android.graphics.drawable.Drawable")
        classPool.importPackage("java.lang.Runnable")
    }
    @Override
    String getName() {
        return "TinyPngPluginTransform" // 任务名称，只要和现有的任务名称不重复即可
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        // 接收的输入数据的类型
        //CLASSES(0x01),
        //RESOURCES(0x02)，assets/目录下的资源，而不是res下的资源
        return TransformManager.CONTENT_CLASS
    }

    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        // 该transform工作的作用域--->见下表
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        //是否增量编译
        //基于Task的上次输出快照和这次输入快照对比，如果相同，则跳过
        return false
    }

    // 向工程内所有Activity的onCreate函数插入toast代码快
    // 工程内源码参与编译的.class，以jar(aar)参与编译的.class，都进行修改
    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        def outputProvider = transformInvocation.outputProvider
        // 1，对inputs 里面 directory 目录下class 文件进行遍历
        // 2，对inputs 里面 jar       包  下class 文件进行遍历
        // 3，符合我们的项目包名，且class文件路径包含Activity.class结尾，
        // 还不能是buildConfig.class,R.class $.class

        transformInvocation.inputs.each {input->
            // 以源码形式在该文件夹子下面参与构建
            // 1，对inputs 里面 directory 目录下class 文件进行遍历
            input.directoryInputs.each {directoryInput->
                println("directoryInput 绝对路径 abs file path: ${directoryInput.file.absolutePath}")
                handleDirectory(directoryInput.file) // file对象是文件目录-修改class

                def dest = outputProvider.getContentLocation(
                        directoryInput.name,
                        directoryInput.contentTypes,
                        directoryInput.scopes,
                        Format.DIRECTORY
                )
                // 把修改之后的file文件，拷贝到输出路径中去
                FileUtils.copyDirectory(directoryInput.file, dest) // 拷贝class
            }
            // 2，对inputs 里面 jar       包  下class 文件进行遍历
            input.jarInputs.each {jarInput->
                println("jarInput 绝对路径 abs file path: ${jarInput.file.absolutePath}")
                // 修改完jar包中class文件后，会再写入到一个新的jar包中，并返回新的jar包文件
                def srcFile = handleJar(jarInput.file)
                // 把修改之后的file文件，拷贝到输出路径中去。为避免jar包重名，需要先做jar的重命名。
                def jarName = jarInput.name
                def md5 = DigestUtils.md5Hex(jarInput.file.absolutePath)
                if (jarName.endsWith(".jar")) {
                    // 截取文件名称，不包含.jar
                    jarName = jarName.substring(0, jarName.length() - 4)
                }
                // 获取jar包的输出路径
//                def dest = outputProvider.getContentLocation(
//                        jarName+md5,
//                        jarInput.name,
//                        jarInput.contentTypes,
//                        jarInput.scopes,
//                        Format.DIRECTORY
//                )
                def dest = outputProvider.getContentLocation(
                        md5+jarName,
                        jarInput.contentTypes,
                        jarInput.scopes,
                        Format.JAR
                )
                // 把修改之后的file文件，拷贝到输出路径中去
                FileUtils.copyFile(srcFile, dest) // 拷贝
            }
        }
        classPool.clearImportedPackages()
    }


    // 处理当前目录里下所有的class文件
    void handleDirectory(File dir) {
        classPool.appendClassPath(dir.absolutePath)
        if (dir.isDirectory()) {
            dir.eachFileRecurse {file->
                def filePath = file.absolutePath
                println("handleDirectory file path:${filePath}")
                // 判断是否符合修改class文件的条件
                if (shouldModifyClass(filePath)) {
                    // 为兼容修改jar包中class场景，将file转化为FileInputStream
                    def inputStream = new FileInputStream(file)
                    def ctClass = modifyClass(inputStream)
                    ctClass.writeFile(dir.name)
                    if (ctClass.isFrozen()) {
                        // class是否被冻结，这个是javasisst中的概念
                        ctClass.defrost()
                    }
                    ctClass.detach() //从classPool中释放，防止内存中常驻

                }
            }
        }
    }
    File handleJar(File jarFile) {
        // 添加类的搜索路径，否则下面classPool.get()查找类时，找不到。
        classPool.appendClassPath(jarFile.absolutePath)
        def inputJarFile = new JarFile(jarFile) // 通过 JarFile才能获取到jar包里面的一个个子文件
        def enumeration = inputJarFile.entries()

        // jar包中class修改后，不能原路写入jar，否则会破坏jar文件结构，需要单独写入一个jar中
        // 将新文件写入到和原文件目录同级别，jarFile.parentFile下。
        def outputJarfile = new File(jarFile.parentFile, "temp_"+jarFile.name) // 这原来写错成 new File(jarFile,*)
        if (outputJarfile.exists())outputJarfile.delete()
        def jarOutputStream = new JarOutputStream(new BufferedOutputStream(new FileOutputStream(outputJarfile)))
        while (enumeration.hasMoreElements()) {
            def inputJarEntry = enumeration.nextElement()
            def inputJarEntryName = inputJarEntry.name

            def outputJarEntry = new JarEntry(inputJarEntryName)
            jarOutputStream.putNextEntry(outputJarEntry)
            println("inputJarEntryName: ${inputJarEntryName}")

            def inputStream = inputJarFile.getInputStream(inputJarEntry)
            if (!shouldModifyClass2(inputJarEntryName)) {
                jarOutputStream.write(IOUtils.toByteArray(inputStream))
                inputStream.close()
                continue
            }
            def ctClass = modifyClass2(inputStream)
//            ctClass.defrost()
            def byteCode = ctClass.toBytecode()
            ctClass.detach()
            inputStream.close()

            jarOutputStream.write(byteCode)
            jarOutputStream.flush()
        }
        inputJarFile.close()
        jarOutputStream.closeEntry()
        jarOutputStream.flush()
        jarOutputStream.close()
        return outputJarfile
    }

    boolean shouldModifyClass2(String filePath) {
        return filePath.contains("androidx/appcompat/widget/AppCompatImageView")
    }
    // 向AppCompatImageView-setImageDrawable 插入不合理大图检测的代码
    /**javassist学习地址：https://zhuanlan.zhihu.com/p/349661837*/
    CtClass modifyClass2(InputStream inputStream) {
        // 通过输入流得到 javassist 中的 CTClass 对象。
        def classFile = new ClassFile(new DataInputStream(new BufferedInputStream(inputStream)))
        println("modifyClass name: ${classFile.name}") // 全类名
        def ctClass = classPool.get(classFile.name)
        if (ctClass.isFrozen()) {
            // class是否被冻结，这个是javasisst中的概念
            ctClass.defrost()
        }
        def drawable = classPool.get("android.graphics.drawable.Drawable") // 这里错写成了bundle
        def method = ctClass.getDeclaredMethod("setImageDrawable", drawable)

        /**start - 创建Runnable的实现类 RunnableImpl，在debug包下*/
        def runnableImpl = classPool.makeClass("org.bagou.proj.main.debug.RunnableImpl")
        // 当CtClass 调用writeFile()、toClass()、toBytecode() 这些方法的时候，
        // Javassist会冻结CtClass Object，对CtClass object的修改将不允许。
        // 这个主要是为了警告开发者该类已经被加载，而JVM是不允许重新加载该类的。
        // 第三个参数表明，要向第三个参数中增加
        CtField viewFiled = new CtField(classPool.get("androidx.appcompat.widget.AppCompatImageView"), "view", runnableImpl)
//        CtField viewFiled = new CtField(classPool.get("android.widget.ImageView"), "view", runnableImpl)
        viewFiled.setModifiers(Modifier.PUBLIC)
        runnableImpl.addField(viewFiled)
        def drawableFiled = new CtField(classPool.get("android.graphics.drawable.Drawable"), "drawable", runnableImpl)
        drawableFiled.setModifiers(Modifier.PUBLIC)
        runnableImpl.addField(drawableFiled)

        // 第二个参数表示，给哪个类添加构造函数
        /**java.lang.VerifyError:
         * Verifier rejected class 包名.RunnableImpl:
         * void 包名.RunnableImpl.<init>(android.view.View, android.graphics.drawable.Drawable)
         * failed to verify: void 包名.RunnableImpl.<init>(android.view.View, android.graphics.drawable.Drawable): [0x3]
         * register v1 has type Reference: android.view.View
         * but expected Reference: androidx.appcompat.widget.AppCompatImageView
         * (declaration of '包名.RunnableImpl' appears in ../base.apk!classes10.dex)*/
        runnableImpl.addConstructor(CtNewConstructor.make("public RunnableImpl(View view, android.graphics.drawable.Drawable drawable) {\n" +
                "            this.view = view;\n" +
                "            this.drawable = drawable;\n" +
                "        }", runnableImpl))

        // 这里表示给类RunnableImpl添加一个接口，如 class RunnableImpl implement Runnable {...}
        runnableImpl.addInterface(classPool.get("java.lang.Runnable"))

        // RunnableImpl中添加run方法
        def run = new CtMethod(CtClass.voidType, "run", null, runnableImpl)
        run.setModifiers(Modifier.PUBLIC)
        run.setBody("{\n" +
                "            int drawableWidth = drawable.getIntrinsicWidth();\n" +
                "            int drawableHeight = drawable.getIntrinsicHeight();\n" +
                "            int width = view.getWidth();\n" +
                "            int height = view.getHeight();\n" +
                "\n" +
                "            if (width*2 <= drawableWidth && height*2 <= drawableHeight) {\n" +
                "                // 提示，图片资源不合理，太大了\n" +
                "                android.util.Log.e(\"LargeImageChecker\", \"不合理大图检测 - bitmap:[\"+\"className:\"+view.getContext().getClass().getSimpleName()+\"]\");\n" +
                "            }\n" +
                "            android.util.Log.e(\"LargeImageChecker\", \"合理大图检测 - bitmap:[\"+\"className:\"+view.getContext().getClass().getSimpleName()+\"]\");\n" +
                "        }")
        runnableImpl.addMethod(run)
        // 接下来，将RunnableImpl写入到本地文件中去
        // 写入的时候，最好是把文件写入到目录 "**/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes"
        // 因为：打包时候就能连同这个class文件一同打包到apk。如果生成到项目工程的别的目录（如项目根目录），打包可能就出现打不进去。
        runnableImpl.writeFile("app/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes")
        // 加载class 类，默认加载到当前线程的ClassLoader中，也可以选择输出的ClassLoader。
        if (runnableImpl.isFrozen()) {
            runnableImpl.defrost()
        }
        runnableImpl.toClass() // 加载这个类
        if (runnableImpl.isFrozen()) {
            runnableImpl.defrost()
        }
        // 增加到class库中去(搜索Classpath载入Class,动态加载classpath的方法)
        classPool.insertClassPath("org.bagou.proj.main.debug.RunnableImpl")
        if (runnableImpl.isFrozen()) {
            runnableImpl.defrost()
        }
        /**end - */
        // RunnableImpl不添加包名，报错-找不到RunnableImpl类。其实在build目录已经生成
        method.insertBefore("if(drawable!=null){post(new org.bagou.proj.main.debug.RunnableImpl(this, drawable));}")
        def message = "使用字节码插桩"
        method.insertAfter("Toast.makeText(this, "+"\""+ message +"\""+", Toast.LENGTH_SHORT).show();")
        return ctClass
    }
    boolean shouldModifyClass (String filePath) {
        println("shouldModifyClass方法-filePath = ${filePath}")
        return (
        filePath.contains("org/bagou/proj")//原先错误为org.wenhuayuan.project
        && filePath.endsWith("Activity.class")
        && !filePath.contains("R.class")
        && !filePath.contains('$')
        && !filePath.contains('R$')
        && !filePath.contains("BuildConfig.class")
        )
    }


    CtClass modifyClass(InputStream inputStream) {// 这错写成了FileInputStream
        // 通过输入流得到 javasssist 中的 CTClass 对象。
        def classFile = new ClassFile(new DataInputStream(new BufferedInputStream(inputStream)))
        println("modifyClass name: ${classFile.name}") // 全类名
        def ctClass = classPool.get(classFile.name)

        if (ctClass.isFrozen()) {
            // class是否被冻结，这个是javasisst中的概念
            ctClass.defrost()
        }
        def bundle = classPool.get("android.os.Bundle") // 这里错写成了bundle
        CtClass[] params = Arrays.asList(bundle).toArray()
        def method = ctClass.getDeclaredMethod("onCreate", params)
        def message = classFile.name
        method.insertAfter("Toast.makeText(this, "+"\""+ message +"\""+", Toast.LENGTH_SHORT).show();")//给每个方法的最后一行添加代码行
        return ctClass
    }
}